# Copyright 2020 The Dawn & Tint Authors
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Check for Jinja2
if (NOT DAWN_JINJA2_DIR)
    message(STATUS "Dawn: Using system jinja2")
    execute_process(
        COMMAND ${Python3_EXECUTABLE} -c "import jinja2"
        RESULT_VARIABLE RET
    )
    if (NOT RET EQUAL 0)
        message(FATAL_ERROR "Dawn: Missing dependencies for code generation, please ensure you have python-jinja2 installed.")
    endif()
else()
    message(STATUS "Dawn: using jinja2 at ${DAWN_JINJA2_DIR}")
    message(STATUS "Dawn: using markupsafe at ${DAWN_MARKUPSAFE_DIR}")
endif()

# Function to invoke a generator_lib.py generator.
#  - SCRIPT is the name of the script to call
#  - OUTPUT_HEADERS will be modified to contain the list of header files (*.h, *.hpp) generated by this generator
#  - OUTPUT_SOURCES will be modified to contain the list of all other files generated by this generator
#  - ARGS are the extra arguments to pass to the script in addition to the base generator_lib.py arguments
#  - PRINT_NAME is the name to use when outputting status or errors
function(DawnGenerator)
    cmake_parse_arguments(PARSE_ARGV 0 arg
        ""
        "SCRIPT;OUTPUT_HEADERS;OUTPUT_SOURCES;OUTPUT_JSONS;OUTPUT_JS;PRINT_NAME"
        "EXTRA_PARAMETERS"
    )
    message(STATUS "Dawn: Configuring DawnGenerator for ${arg_PRINT_NAME}.")

    if (arg_UNPARSED_ARGUMENTS)
      message(FATAL_ERROR
        "Unparsed arguments for DawnGenerator: "
        "${arg_UNPARSED_ARGUMENTS}")
    endif ()

    # Build the set of args common to all invocation of that generator.
    set(BASE_ARGS
        ${Python3_EXECUTABLE}
        ${arg_SCRIPT}
        --template-dir
        "${DAWN_TEMPLATE_DIR}"
        --root-dir
        "${Dawn_SOURCE_DIR}"
        --output-dir
        "${DAWN_BUILD_GEN_DIR}"
        ${arg_EXTRA_PARAMETERS}
    )
    if (DAWN_JINJA2_DIR)
        list(APPEND BASE_ARGS --jinja2-path ${DAWN_JINJA2_DIR})
    endif()
    if (DAWN_MARKUPSAFE_DIR)
        list(APPEND BASE_ARGS --markupsafe-path ${DAWN_MARKUPSAFE_DIR})
    endif()

    # Call the generator to get the list of its dependencies.
    execute_process(
        COMMAND ${BASE_ARGS} --print-cmake-dependencies
        OUTPUT_VARIABLE DEPENDENCIES
        RESULT_VARIABLE RET
    )
    if (NOT RET EQUAL 0)
        message(FATAL_ERROR "Dawn: Failed to get the dependencies for ${arg_PRINT_NAME}. Base args are '${BASE_ARGS}'.")
    endif()

    # Ask CMake to re-run if any of the dependencies changed as it might modify the build graph.
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${DEPENDENCIES})

    # Call the generator to get the list of its outputs.
    execute_process(
        COMMAND ${BASE_ARGS} --print-cmake-outputs
        OUTPUT_VARIABLE OUTPUTS
        RESULT_VARIABLE RET
    )
    if (NOT RET EQUAL 0)
        message(FATAL_ERROR "Dawn: Failed to get the outputs for ${arg_PRINT_NAME}. Base args are '${BASE_ARGS}'.")
    endif()

    # Add the custom command that calls the generator.
    add_custom_command(
        COMMAND ${BASE_ARGS}
        DEPENDS ${DEPENDENCIES}
        OUTPUT ${OUTPUTS}
        COMMENT "Dawn: Generating files for ${arg_PRINT_NAME}."
    )

    # Populate the list of outputs.
    set(headers)
    set(sources)
    set(jsons)
    set(js)
    foreach (filename IN LISTS OUTPUTS)
        get_filename_component(extension "${filename}" EXT)
        if (extension MATCHES "(h|hpp)")
            list(APPEND headers "${filename}")
        elseif (extension MATCHES "(json)")
            list(APPEND jsons "${filename}")
        elseif (extension MATCHES "(js)")
            list(APPEND js "${filename}")
        else ()
            list(APPEND sources "${filename}")
        endif ()
    endforeach ()

    if (sources AND NOT arg_OUTPUT_SOURCES)
        message(FATAL_ERROR
            "This function generated source files, although an output variable was not defined!"
            "Please provide a variable name for OUTPUT_SOURCES.")
    endif ()

    if (headers AND NOT arg_OUTPUT_HEADERS)
        message(FATAL_ERROR
            "This function generated header files, although an output variable was not defined!"
            "Please provide a variable name for OUTPUT_HEADERS.")
    endif ()

    if (jsons AND NOT arg_OUTPUT_JSONS)
        message(FATAL_ERROR
            "This function generated JSON files, although an output variable was not defined!"
            "Please provide a variable name for OUTPUT_JSONS.")
    endif ()

    if (js AND NOT arg_OUTPUT_JS)
        message(FATAL_ERROR
            "This function generated JS files, although an output variable was not defined!"
            "Please provide a variable name for OUTPUT_JS.")
    endif ()

    set(${arg_OUTPUT_SOURCES} ${sources} PARENT_SCOPE)
    set(${arg_OUTPUT_HEADERS} ${headers} PARENT_SCOPE)
    set(${arg_OUTPUT_JSONS} ${jsons} PARENT_SCOPE)
    set(${arg_OUTPUT_JS} ${js} PARENT_SCOPE)

    # Prior to CMake 3.20 the GENERATED property is local to a directory which means that putting
    # generated headers in INTERFACE properties causes dependent targets to complain that they
    # cannot find the file. (because they don't see it as generated and want to check is is
    # actually on the filesystem).
    # Work around this by generating the files once if they aren't present at configuration time.
    set(needToGenerate OFF)
    foreach(path ${OUTPUTS})
        if (NOT EXISTS ${path})
            set(needToGenerate ON)
        endif()
    endforeach()

    if (${needToGenerate})
        message(STATUS "Dawn: Generating initial versions of files for ${arg_PRINT_NAME}.")
        execute_process(COMMAND ${BASE_ARGS} RESULT_VARIABLE RET)
        if (NOT RET EQUAL 0)
            message(FATAL_ERROR "Dawn: Failed to generate the initial version of files for ${arg_PRINT_NAME}. Base args are '${BASE_ARGS}'.")
        endif()
    endif()
endfunction()

# Helper function to call dawn_generator.py:
#  - TARGET is the generator target to build
#  - OUTPUT_HEADERS, OUTPUT_SOURCES, and PRINT_NAME are like for DawnGenerator
function(DawnJSONGenerator)
    cmake_parse_arguments(PARSE_ARGV 0 arg
        ""
        "TARGET;OUTPUT_SOURCES;OUTPUT_HEADERS;OUTPUT_JSONS;OUTPUT_JS;PRINT_NAME"
        ""
    )
    DawnGenerator(
        SCRIPT "${Dawn_SOURCE_DIR}/generator/dawn_json_generator.py"
        PRINT_NAME "${arg_PRINT_NAME}"
        OUTPUT_HEADERS HEADERS
        OUTPUT_SOURCES SOURCES
        OUTPUT_JSONS JSONS
        OUTPUT_JS JS
        EXTRA_PARAMETERS
             --dawn-json
             "${Dawn_SOURCE_DIR}/src/dawn/dawn.json"
             --wire-json
             "${Dawn_SOURCE_DIR}/src/dawn/dawn_wire.json"
             --kotlin-json
             "${Dawn_SOURCE_DIR}/src/dawn/dawn_kotlin.json"
             --targets
             ${arg_TARGET}
             ${arg_UNPARSED_ARGUMENTS}
    )

    if (SOURCES AND NOT arg_OUTPUT_SOURCES)
        message(FATAL_ERROR
            "This function generated source files, although an output variable was not defined! "
            "Please provide a variable name for OUTPUT_SOURCES.")
    elseif (arg_OUTPUT_SOURCES AND NOT SOURCES)
        message(WARNING
            "This function did not generate source files. The OUTPUT_SOURCES argument is unnecessary.")
    endif ()

    if (HEADERS AND NOT arg_OUTPUT_HEADERS)
        message(FATAL_ERROR
            "This function generated header files, although an output variable was not defined! "
            "Please provide a variable name for OUTPUT_HEADERS.")
    elseif (arg_OUTPUT_HEADERS AND NOT HEADERS)
        message(WARNING
            "This function did not generate header files. The OUTPUT_HEADERS argument is unnecessary.")
    endif ()

    if (JSONS AND NOT arg_OUTPUT_JSONS)
        message(FATAL_ERROR
            "This function generated JSON files, although an output variable was not defined! "
            "Please provide a variable name for OUTPUT_JSONS.")
    elseif (arg_OUTPUT_JSONS AND NOT JSONS)
        message(WARNING
            "This function did not generate JSON files. The OUTPUT_JSONS argument is unnecessary.")
    endif ()

    if (JS AND NOT arg_OUTPUT_JS)
        message(FATAL_ERROR
            "This function generated JS files, although an output variable was not defined! "
            "Please provide a variable name for OUTPUT_JS.")
    elseif (arg_OUTPUT_JS AND NOT JS)
        message(WARNING
            "This function did not generate JS files. The OUTPUT_JS argument is unnecessary.")
    endif ()

    # Forward the result up one more scope
    set(${arg_OUTPUT_SOURCES} ${SOURCES} PARENT_SCOPE)
    set(${arg_OUTPUT_HEADERS} ${HEADERS} PARENT_SCOPE)
    set(${arg_OUTPUT_JSONS} ${JSONS} PARENT_SCOPE)
    set(${arg_OUTPUT_JS} ${JS} PARENT_SCOPE)
endfunction()
